﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace Sys.Excel
{
    public abstract class ExcelExporterBase<TWorkBook, TWorkSheet, TCellStyle> : IExcelExporter
        where TWorkBook : class
        where TWorkSheet : class
        where TCellStyle : class
    {
        protected ExcelExporterBase()
        {

        }

        public Task BuildSheetsAsync(Stream stream, IEnumerable<Settings> settingsList, bool useOldVersion = false)
        {
            if (stream == null)
            {
                throw new ArgumentNullException(nameof(stream));
            }

            if (settingsList == null)
            {
                throw new ArgumentNullException(nameof(settingsList));
            }

            var workbook = this.CreateWorkBook(useOldVersion);
            var stylesDict = new Dictionary<CellStyle, TCellStyle>();

            try
            {
                foreach (Settings settings in settingsList.SelectMany(set => this.ToPagerSettings(set)))
                {
                    if (settings == null)
                    {
                        continue;
                    }

                    var total = 0;
                    var len = settings.LeafNodes.Count;
                    var start_row = (int)settings.StartRowIndex;
                    var start_col = (int)settings.StartColIndex;
                    var dataSource = settings.DataSource as IEnumerable<object> ?? this.Get_DataSource(settings, out _);
                    var valueGetter = settings.CellValueGetter ?? this.Default_ValueGetter;
                    var sheet = this.CreateWorkSheet(workbook, string.IsNullOrWhiteSpace(settings.Name) ? null : settings.Name, settings);

                    IList<IDictionary<string, object>> dicts = null;

                    try
                    {
                        this.BuildHeader(
                            workbook, sheet, settings,
                            settings.RootNodes, start_row, start_col);

                        this.SetHeaderStyle(
                            workbook, sheet, settings,
                            stylesDict, start_row, start_col);

                        this.BuildBody(
                            workbook, sheet, settings,
                            Tuple.Create(dataSource, valueGetter),
                            stylesDict, start_row + settings.MaxLevel + 1, start_col,
                            out dicts, out total);

                        this.MergeBodyCells(
                            workbook, sheet, settings,
                            dicts, start_row + settings.MaxLevel + 1, start_col);

                        if (settings.TotalValueGetter != null)
                        {
                            this.AddTotalRow(
                                workbook, sheet, settings, dicts,
                                stylesDict,
                                start_row + settings.MaxLevel + 1 + total,
                                start_col);
                        }
                    }
                    finally
                    {
                        dicts?.Clear();
                    }

                    if (settings.SheetBuilded != null)
                    {
                        this.SheetBuilded(workbook, sheet, settings, stylesDict, total, len);
                    }

                    if (settings.FreezeHeader)
                    {
                        this.FreezePane(workbook, sheet, start_row + settings.MaxLevel + 1, start_col);
                    }

                    if (settings.ColumnsAutoWidth)
                    {
                        this.AutoFillColumns(workbook, sheet, settings, start_col);
                    }
                    else
                    {
                        this.SetColumnsWidth(
                            workbook, sheet, settings,
                            settings.LeafNodes.Select(col => col.Width).ToArray(),
                            start_col);
                    }

                    this.SetDefaultRowHeight(workbook, sheet, settings.RowHeight);
                }

                using (stream)
                {
                    this.Save(workbook, useOldVersion, stream);
                }
            }
            finally
            {
                stylesDict.Clear();

                this.CloseWorkBook(workbook);
            }

            return Task.CompletedTask;
        }

        protected abstract TWorkBook CreateWorkBook(bool useOldVersion);

        protected abstract TWorkSheet CreateWorkSheet(TWorkBook workbook, string name, Settings settings);

        protected abstract TCellStyle SetCellStyle(TWorkBook workbook, TCellStyle style, CellStyle cellStyle);

        protected abstract TCellStyle GetCellStyle(TWorkBook workbook, TWorkSheet sheet, Settings settings, int row, int col);

        protected abstract void SetDefaultRowHeight(TWorkBook workbook, TWorkSheet sheet, uint height);

        protected abstract void SetCell(TWorkBook workbook, TWorkSheet sheet, int row, int col, Settings settings, ColNode node, object value, TCellStyle style, bool setValue = true, bool setStyle = true);

        protected abstract void MergeCells(TWorkBook workbook, TWorkSheet sheet, int startRow, int endRow, int startCol, int endCol, Settings settings);

        protected abstract void FreezePane(TWorkBook workbook, TWorkSheet sheet, int rowSplit, int colSplit);

        protected abstract void SetColumnsWidth(TWorkBook workbook, TWorkSheet sheet, Settings settings, uint[] widthArray, int startCol);

        protected abstract void AutoFillColumns(TWorkBook workbook, TWorkSheet sheet, Settings settings, int startCol);

        protected abstract void Save(TWorkBook workbook, bool useOldVersion, Stream stream);

        protected abstract void CloseWorkBook(TWorkBook workbook);

        private Tv TryGet<Tk, Tv>(IDictionary<Tk, Tv> dic, Tk k)
        {
            var v = default(Tv);

            if (dic != null)
            {
                dic.TryGetValue(k, out v);
            }

            return v;
        }

        private TCellStyle Inner_GetCellStyle(
            TWorkBook workbook, IDictionary<CellStyle, TCellStyle> stylesDict,
            TCellStyle style, CellStyle cellStyle)
        {
            if (!stylesDict.TryGetValue(cellStyle, out TCellStyle t_style))
            {
                stylesDict[cellStyle] = t_style = this.SetCellStyle(workbook, style, cellStyle);
            }

            return t_style;
        }

        private CellStyle CheckStyle(CellStyle? style, CellStyle defaultStyle)
        {
            var st = style ?? defaultStyle;

            if (st.FgColor == null)
            {
                st.FgColor = defaultStyle.FgColor;
            }

            if (st.BgColor == null)
            {
                st.BgColor = defaultStyle.BgColor;
            }

            if (st.TextAlign == null)
            {
                st.TextAlign = defaultStyle.TextAlign;
            }

            if (st.FontWeight == null)
            {
                st.FontWeight = defaultStyle.FontWeight;
            }

            return st;
        }

        private Func<Settings.CellValueContext, object> GetCellValueGetter(Type rowType)
        {
            if (rowType == null)
            {
                return this.Default_ValueGetter;
            }

            if (typeof(DataRow).IsAssignableFrom(rowType))
            {
                return ctx => Utils.GetValue(ctx.Row as DataRow, ctx.Column.Field);
            }

            if (typeof(IDictionary<string, object>).IsAssignableFrom(rowType))
            {
                return ctx => Utils.GetValue(ctx.Row as IDictionary<string, object>, ctx.Column.Field);
            }

            if (typeof(IDictionary).IsAssignableFrom(rowType))
            {
                return ctx => Utils.GetValue(ctx.Row as IDictionary, ctx.Column.Field);
            }

            var props = rowType
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .ToDictionary(p => p.Name, p => Dynamic.PropertyGetter(p));

            var fiels = rowType
                .GetFields(BindingFlags.Public | BindingFlags.Instance)
                .ToDictionary(f => f.Name, f => Dynamic.FieldGetter(f));

            return ctx =>
            {
                var obj = ctx.Row;
                var field = ctx.Column.Field;

                if (obj == null || field == null)
                {
                    return null;
                }

                if (props.TryGetValue(field, out var prop))
                {
                    return prop.Invoke(obj);
                }

                if (fiels.TryGetValue(field, out var fiel))
                {
                    return fiel.Invoke(obj);
                }

                return null;
            };
        }

        private object Default_ValueGetter(Settings.CellValueContext ctx)
        {
            var obj = ctx.Row;
            var field = ctx.Column.Field;

            if (obj == null || field == null)
            {
                return null;
            }

            if (obj is DataRow row)
            {
                return Utils.GetValue(row, field);
            }
            else if (obj is IDictionary<string, object> dict_g)
            {
                return Utils.GetValue(dict_g, field);
            }
            else if (obj is IDictionary dict)
            {
                return Utils.GetValue(dict, field);
            }
            else
            {
                var obj_type = obj.GetType();
                var prop = obj_type.GetProperty(field, BindingFlags.Public | BindingFlags.Instance);

                if (prop != null)
                {
                    return Dynamic.PropertyGetter(prop)(obj);
                }

                var fiel = obj_type.GetField(field, BindingFlags.Public | BindingFlags.Instance);

                if (fiel != null)
                {
                    return Dynamic.FieldGetter(fiel)(obj);
                }
            }

            return null;
        }

        private IEnumerable<object> Get_DataSource(Settings settings, out Type rowType)
        {
            var dataSource = this.Get_DataSource(settings, settings.DataSource);

            rowType = null;

            if (dataSource == null)
            {
                return null;
            }

            if (dataSource is DataTable dataTable)
            {
                rowType = typeof(DataRow);

                return dataTable.AsEnumerable();
            }

            var type = dataSource.GetType();

            if (this.Is_Generic_Interface(type, typeof(IEnumerable<>), out var outType))
            {
                rowType = outType.GetGenericArguments()[0];

                return dataSource as IEnumerable<object>;
            }

            if (dataSource is ICollection collection)
            {
                return collection.Cast<object>();
            }

            if (dataSource is IEnumerable enumerable)
            {
                return enumerable.Cast<object>();
            }

            return null;
        }

        private object Get_DataSource(Settings settings, object dataSource)
        {
            if (dataSource == null)
            {
                return null;
            }

            var type = dataSource.GetType();

            if (type.IsGenericType)
            {
                var g_type = type.GetGenericTypeDefinition();

                if (g_type == typeof(Func<>))
                {
                    dataSource = ((Delegate)dataSource).DynamicInvoke();
                }
                else if (g_type == typeof(Lazy<>))
                {
                    dataSource = type.GetProperty(nameof(Lazy<object>.Value)).GetValue(dataSource);
                }
            }

            if (dataSource is IDataSource idataSource)
            {
                dataSource = idataSource.Get(settings);
            }
            else if (dataSource is DataSet dataSet)
            {
                dataSource = dataSet.Tables.Count > 0 ? dataSet.Tables[0] : null;
            }

            return dataSource;
        }

        private bool Is_Generic_Interface(Type target, Type interfaceGenericTypeDefinition, out Type outType)
        {
            outType = null;

            if (target == null)
            {
                return false;
            }

            Type igd_type = interfaceGenericTypeDefinition;

            if (igd_type.IsInterface && igd_type.IsGenericTypeDefinition)
            {
                outType = target.GetInterfaces().FirstOrDefault(type => type.IsGenericType && type.GetGenericTypeDefinition() == igd_type);

                return outType != null;
            }

            return false;
        }

        private IEnumerable<Settings> ToPagerSettings(Settings settings)
        {
            if (settings == null)
            {
                yield break;
            }

            if (settings.DataSource == null)
            {
                yield return settings;
                yield break;
            }

            var dataSource = this.Get_DataSource(settings, out var rowType);

            if (settings.CellValueGetter == null)
            {
                settings.CellValueGetter = this.GetCellValueGetter(rowType);
            }

            if (dataSource == null || settings.PageSize == 0)
            {
                yield return settings;
                yield break;
            }

            int index = 0;

            foreach (var pager in dataSource.ToPager((int)settings.PageSize))
            {
                yield return new Settings(settings, index)
                {
                    SheetBuilded = settings.SheetBuilded,
                    CellValueGetter = settings.CellValueGetter,
                    TotalValueGetter = settings.TotalValueGetter,
                    ColumnStyleGetter = settings.ColumnStyleGetter,
                    ColumnsAutoWidth = settings.ColumnsAutoWidth,
                    DataSource = pager,
                    FreezeHeader = settings.FreezeHeader,
                    HeaderCellStyle = settings.HeaderCellStyle,
                    Name = string.Format("{0}({1})", settings.Name, index + 1),
                    PageSize = 0,
                    RowHeight = settings.RowHeight,
                    RootNodes = settings.RootNodes,
                    RowStyleGetter = settings.RowStyleGetter,
                    StartColIndex = settings.StartColIndex,
                    StartRowIndex = settings.StartRowIndex
                };

                index++;
            }
        }

        private void BuildHeader(
            TWorkBook workbook,
            TWorkSheet sheet,
            Settings settings,
            IList<ColNode> p_nodes,
            int startRowIndex = 0, int startColIndex = 0)
        {
            if (p_nodes == null || p_nodes.Count == 0)
            {
                return;
            }

            for (int i = 0; i < p_nodes.Count; i++)
            {
                var p_node = p_nodes[i];
                var col_index = startColIndex;
                var row_index = startRowIndex + p_node.Level_Index;
                var is_leaf = p_node.ChildColNodes == null || p_node.ChildColNodes.Count == 0;

                this.SetCell(
                    workbook, sheet,
                    row_index, col_index, settings, p_node, p_node.Title,
                    default(TCellStyle), true, false);

                if (!is_leaf)
                {
                    this.BuildHeader(
                        workbook, sheet, settings, p_node.ChildColNodes,
                        startRowIndex, col_index);
                }

                startColIndex += p_node.LeafNodes.Count;

                this.MergeCells(
                    workbook, sheet, row_index,
                    is_leaf ? startRowIndex + settings.MaxLevel : row_index, col_index,
                    startColIndex - 1, settings);
            }
        }

        private void SetHeaderStyle(
            TWorkBook workbook,
            TWorkSheet sheet,
            Settings settings,
            IDictionary<CellStyle, TCellStyle> stylesDict,
            int startRowIndex, int startColIndex)
        {
            var cusStyle = this.CheckStyle(settings.HeaderCellStyle, CellStyle.Header);

            for (int i = 0; i < settings.MaxLevel + 1; i++)
            {
                for (int j = 0; j < settings.LeafNodes.Count; j++)
                {
                    var cellStyle = this.GetCellStyle(workbook, sheet, settings, i, j);

                    var style = this.Inner_GetCellStyle(workbook, stylesDict, cellStyle, cusStyle);

                    this.SetCell(
                        workbook, sheet,
                        startRowIndex + i, startColIndex + j, settings, null, null,
                        style, false);
                }
            }
        }

        private void BuildBody(
            TWorkBook workbook, TWorkSheet sheet,
            Settings settings, Tuple<IEnumerable<object>, Func<Settings.CellValueContext, object>> tuple,
            IDictionary<CellStyle, TCellStyle> stylesDict, int startRowIndex, int startColIndex,
            out IList<IDictionary<string, object>> dicts,
            out int total)
        {
            int i = 0;

            dicts = new List<IDictionary<string, object>>();

            total = i;

            if (tuple == null)
            {
                return;
            }

            var dataList = tuple.Item1;

            if (dataList == null)
            {
                return;
            }

            var formatterArray = settings.LeafNodes
                .Select(node =>
                {
                    if (node == null)
                    {
                        return null;
                    }

                    var formatter = node.Formatter;

                    if (formatter == null && node.FormatterName != null)
                    {
                        Settings._dictFormatters.TryGetValue(node.FormatterName, out formatter);
                    }

                    return formatter;
                })
                .ToArray();

            var columnsStyles = settings.LeafNodes
                .Select((node, j) => settings.ColumnStyleGetter == null ? null : settings.ColumnStyleGetter(j, node))
                .ToArray();

            var mergeNodes = settings.LeafNodes
                .Select(node =>
                {
                    if (!string.IsNullOrWhiteSpace(node.MergeField) && string.Compare(node.Field, node.MergeField) != 0)
                    {
                        return new ColNode()
                        {
                            Field = node.MergeField,
                            Title = node.Title,
                            Width = node.Width,
                            FormatterName = node.FormatterName,
                            ChildColNodes = node.ChildColNodes,
                            Formatter = node.Formatter
                        };
                    }

                    return null;
                })
                .ToArray();

            var context = new Settings.CellValueContext();

            foreach (var obj in dataList)
            {
                var dict = new Dictionary<string, object>();
                var row_style = settings.RowStyleGetter == null ? null : settings.RowStyleGetter.Invoke(i, obj);

                for (int j = 0, n_len = settings.LeafNodes.Count; j < n_len; j++)
                {
                    var node = settings.LeafNodes[j];

                    if (node == null)
                    {
                        continue;
                    }

                    var formatter = formatterArray[j];
                    var cell_val = tuple.Item2.Invoke(context.Reset(obj, i, node, j, this.Default_ValueGetter));
                    var style = this.GetCellStyle(workbook, sheet, settings, startRowIndex + i, startColIndex + j);

                    dict[node.Field + j] = cell_val;

                    var new_node = mergeNodes[j];

                    if (new_node != null)
                    {
                        dict[node.MergeField + j] = tuple.Item2.Invoke(context.Reset(obj, i, new_node, j, this.Default_ValueGetter, true));
                    }

                    var cusStyle = this.CheckStyle(row_style ?? columnsStyles[j], CellStyle.Body);

                    this.SetCell(workbook, sheet,
                        startRowIndex + i, startColIndex + j, settings,
                        node, formatter == null ? cell_val : formatter.Invoke(i, j, obj, cell_val),
                        this.Inner_GetCellStyle(workbook, stylesDict, style, cusStyle),
                        cell_val != null);
                }

                dicts.Add(dict);
                i++;
            }

            total = i;
        }

        private void MergeBodyCells(
            TWorkBook workbook, TWorkSheet sheet,
            Settings settings, IList<IDictionary<string, object>> dicts,
            int startRowIndex, int startColIndex)
        {
            if (dicts == null)
            {
                return;
            }

            for (int i = 0, n_len = settings.LeafNodes.Count; i < n_len; i++)
            {
                var node = settings.LeafNodes[i];

                if (node != null && !string.IsNullOrWhiteSpace(node.MergeField))
                {
                    var count = 0;
                    var first_i = 0;
                    var key = node.MergeField + i;
                    var values = dicts.Select(dic => this.TryGet(dic, key));
                    var listStrs = values.Select(obj => Convert.ToString(obj)).ToList();
                    var list_count = listStrs.Count;

                    string first_val = null;

                    for (int j = 0; j < list_count + 1; j++)
                    {
                        string str_j = null;

                        if (j == list_count || (str_j = listStrs[j]) != first_val)
                        {
                            if (count > 0)
                            {
                                this.MergeCells(
                                    workbook, sheet,
                                    startRowIndex + first_i, startRowIndex + first_i + count,
                                    startColIndex + i, startColIndex + i, settings);
                            }

                            count = 0;

                            first_i = j;

                            first_val = str_j;
                        }
                        else
                        {
                            count++;
                        }
                    }
                }
            }
        }

        private void AddTotalRow(
            TWorkBook workbook, TWorkSheet sheet,
            Settings settings, IList<IDictionary<string, object>> dicts,
            IDictionary<CellStyle, TCellStyle> stylesDict,
            int startRowIndex, int startColIndex)
        {
            if (dicts == null)
            {
                return;
            }

            for (int i = 0, n_len = settings.LeafNodes.Count; i < n_len; i++)
            {
                var node = settings.LeafNodes[i];

                if (node != null)
                {
                    var key = node.Field + i;
                    var values = dicts.Select(dic => this.TryGet(dic, key)).ToList();
                    var totalValue = settings.TotalValueGetter.Invoke(new Settings.TotalValueContext(settings, i, node, values));
                    var style = this.GetCellStyle(workbook, sheet, settings, startRowIndex, startColIndex + i);

                    this.SetCell(workbook, sheet,
                        startRowIndex, startColIndex + i, settings,
                        node, totalValue,
                        this.Inner_GetCellStyle(workbook, stylesDict, style, CellStyle.Header),
                        totalValue != null);
                }
            }
        }

        private void SheetBuilded(
            TWorkBook workbook, TWorkSheet sheet,
            Settings settings, IDictionary<CellStyle, TCellStyle> stylesDict,
            int bodyRowCount, int colCount)
        {
            var start_col = (int)settings.StartColIndex;

            settings.SheetBuilded.Invoke(
                new Settings.SheetBuildedContext(
                    settings, settings.MaxLevel, bodyRowCount, colCount,
                    settings.TotalValueGetter != null,
                    (rowIndex, colIndex, value) =>
                    {
                        var index = colIndex - start_col;

                        this.SetCell(
                            workbook, sheet,
                            rowIndex, colIndex, settings,
                            index < 0 ? null : settings.LeafNodes[index], value, null, true, false);
                    },
                    (rowIndex, colIndex, style) =>
                    {
                        var index = colIndex - start_col;
                        var t_style = this.GetCellStyle(workbook, sheet, settings, rowIndex, colIndex);

                        this.SetCell(
                            workbook, sheet,
                            rowIndex, colIndex, settings,
                            index < 0 ? null : settings.LeafNodes[index], null,
                            this.Inner_GetCellStyle(workbook, stylesDict, t_style, style), false, true);
                    },
                    (startRow, endRow, startCol, endCol) =>
                    {
                        this.MergeCells(
                            workbook, sheet,
                            startRow, endRow, startCol, endCol, settings);
                    }));
        }
    }
}